/*
Copyright (c) 2010.12.27-2012.12.27, trydofor.com :-) All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

- Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.

- Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

- Neither the name of the trydofor.com nor the names of its contributors may
  be used to endorse or promote products derived from this software without
  specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
 */

package com.trydofor.id.a9id;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;

import com.trydofor.id.core.A9DU;
import com.trydofor.id.core.ApiExceptions;
import com.trydofor.id.core.Range;

/**
 * A Domain Unique IDentifier.
 * 
 * @author Shi Rongjiu (www.trydofor.com)
 * @see <a href="http://code.google.com/p/a9id/wiki/A9ID">A9ID Specification</a>
 * @since 0.9
 */
@Immutable
public final class A9ID extends Number implements A9DU, Comparable<A9ID> {
    
    // serialize yyyyMMdd
    private static final long    serialVersionUID  = 20110214L;
    
    // worker
    private static final int     BIT_WORKER        = 11;
    private static final long    MIN_WORKER        = 1L;                                      // 1
    private static final long    MAX_WORKER        = (1L << BIT_WORKER) - 1;                  // (B)'1'*11
    private static final Range   RANGE_WORKER      = new Range(MIN_WORKER, MAX_WORKER);
    
    // second
    private static final int     BIT_SECOND        = 32;
    private static final long    MIN_SECOND        = 0L;                                      // 0
    private static final long    MAX_SECOND        = (1L << BIT_SECOND) - 1;                  // (B)'1'*32
    private static final Range   RANGE_SECOND      = new Range(MIN_SECOND, MAX_SECOND);
    
    // serial
    private static final int     BIT_SERIAL        = 20;
    private static final long    MIN_SERIAL        = 0L;                                      // 0
    private static final long    MAX_SERIAL        = (1L << BIT_SERIAL) - 1;                  // (B)'1'*20
    private static final Range   RANGE_SERIAL      = new Range(MIN_SERIAL, MAX_SERIAL);
    
    // number
    private static final int     BIT_NUMBER        = BIT_WORKER + BIT_SECOND + BIT_SERIAL;
    private static final long    MIN_NUMBER        = 1L << (BIT_SECOND + BIT_SERIAL);         // 2^52
    private static final long    MAX_NUMBER        = (1L << BIT_NUMBER) - 1;                  // (B)'1'*63
    private static final Range   RANGE_NUMBER_BLK  = new Range(MIN_NUMBER, MAX_NUMBER);
    private static final Range   RANGE_NUMBER_RED  = RANGE_NUMBER_BLK.negative();
    
    // static
    private static final int     BIT_STATIC        = 20;
    private static final long    MAX_STATIC        = (1L << BIT_STATIC) - 1;
    private static final Range   RANGE_STATIC      = new Range(0L, MAX_STATIC);
    private static final Range   RANGE_SECOND_STC  = new Range(0L, 0L);
    private static final Range   RANGE_STATIC_BLK  = new Range(0L, MAX_STATIC);
    private static final Range   RANGE_STATIC_RED  = RANGE_STATIC_BLK.negative();
    
    // system
    private static final int     BIT_SYSTEM        = 32;
    private static final long    MIN_SYSTEM        = 1L << BIT_SYSTEM;
    private static final long    MAX_SYSTEM        = (1L << (BIT_SECOND + BIT_SERIAL)) - 1;
    private static final long    MIN_SECOND_SYSTEM = 1L << (BIT_SYSTEM - BIT_STATIC);
    private static final Range   RANGE_SECOND_SYS  = new Range(MIN_SECOND_SYSTEM, MAX_SECOND);
    private static final Range   RANGE_SYSTEM_BLK  = new Range(MIN_SYSTEM, MAX_SYSTEM);
    private static final Range   RANGE_SYSTEM_RED  = RANGE_SYSTEM_BLK.negative();
    
    // format
    private static final String  STRING_JOINER     = "-";
    private static final String  PATTERN_STRING    = "^-?(\\d{1,4})-(\\d{1,10})-(\\d{1,7})$";
    private static final String  PATTERN_NUMBER    = "^-?\\d{1,19}$";
    private static final Pattern REGEXP_STRING     = Pattern.compile(PATTERN_STRING);
    private static final Pattern REGEXP_NUMBER     = Pattern.compile(PATTERN_NUMBER);
    
    /**
     * get the bit count of worker
     * 
     * @return bit count.
     */
    public static int getBitOfWorker() {
        return BIT_WORKER;
    }
    
    /**
     * get the bit count of second
     * 
     * @return bit count
     */
    public static int getBitOfSecond() {
        return BIT_SECOND;
    }
    
    /**
     * get the bit count of serial
     * 
     * @return bit count.
     */
    public static int getBitOfSerial() {
        return BIT_SERIAL;
    }
    
    /**
     * get the black range of static a9ic.
     * 
     * @return black range of static a9ic.
     */
    public static Range getRangeOfStatic() {
        return RANGE_STATIC;
    }
    
    /**
     * get the black range of system a9ic.
     * 
     * @return black range of system a9ic.
     */
    public static Range getRangeOfSystem() {
        return RANGE_SYSTEM_BLK;
    }
    
    /**
     * get the range of worker
     * 
     * @return range of worker.
     */
    public static Range getRangeOfWorker() {
        return RANGE_WORKER;
    }
    
    /**
     * get the range of second
     * 
     * @return range of second
     */
    public static Range getRangeOfSecond() {
        return RANGE_SECOND;
    }
    
    /**
     * get the range of serial
     * 
     * @return range of serial.
     */
    public static Range getRangeOfSerial() {
        return RANGE_SERIAL;
    }
    
    /**
     * get the pattern of string format
     * 
     * @return pattern string
     */
    public static String getPatternOfString() {
        return PATTERN_STRING;
    }
    
    /**
     * get the pattern of number format
     * 
     * @return pattern string
     */
    public static String getPatternOfNumber() {
        return PATTERN_NUMBER;
    }
    
    /**
     * make new instance by string
     * 
     * @param a9id @Nonnull '#-#-#' or '######'
     * @throws NumberFormatException if invalid format
     * @throws NullPointerException if parameter is null
     * @throws IllegalArgumentException if out of range
     * @return @Nonnull a9id
     */
    @Nonnull
    public static A9ID of(@Nonnull String a9id) {
        ApiExceptions.checkNullPointer(a9id);
        
        if (REGEXP_NUMBER.matcher(a9id).find()) {
            return of(Long.parseLong(a9id));
        }
        else {
            Matcher m = REGEXP_STRING.matcher(a9id);
            if (m.find() && m.groupCount() == 3) {
                int wk = Integer.parseInt(m.group(1));
                long ts = Long.parseLong(m.group(2));
                int sn = Integer.parseInt(m.group(3));
                return new A9ID(wk, ts, sn);
            }
            else {
                throw new NumberFormatException("bad pattern[" + a9id + "],need:" + PATTERN_STRING);
            }
        }
    }
    
    /**
     * make new instance by number
     * 
     * @param a9id long value
     * @throws IllegalArgumentException if out of range
     * @return @Nonnull a9id
     */
    @Nonnull
    public static A9ID of(long a9id) {
        
        final long pv;
        if (a9id >= 0) {
            boolean isNumber = RANGE_NUMBER_BLK.contains(a9id);
            boolean isStatic = RANGE_STATIC_BLK.contains(a9id);
            boolean isSystem = RANGE_SYSTEM_BLK.contains(a9id);
            if (!(isNumber || isStatic || isSystem)) {
                ApiExceptions.checkRangeNotContains(RANGE_NUMBER_BLK, a9id);
                ApiExceptions.checkRangeNotContains(RANGE_STATIC_BLK, a9id);
                ApiExceptions.checkRangeNotContains(RANGE_SYSTEM_BLK, a9id);
            }
            pv = a9id;
        }
        else {
            boolean isNumber = RANGE_NUMBER_RED.contains(a9id);
            boolean isStatic = RANGE_STATIC_RED.contains(a9id);
            boolean isSystem = RANGE_SYSTEM_RED.contains(a9id);
            
            if (!(isNumber || isStatic || isSystem)) {
                ApiExceptions.checkRangeNotContains(RANGE_NUMBER_RED, a9id);
                ApiExceptions.checkRangeNotContains(RANGE_STATIC_RED, a9id);
                ApiExceptions.checkRangeNotContains(RANGE_SYSTEM_RED, a9id);
            }
            pv = -a9id;
        }
        
        int wk = (int) (pv >>> (BIT_SECOND + BIT_SERIAL));
        long ts = (pv >>> BIT_SERIAL) & MAX_SECOND;
        int sn = (int) (pv & MAX_SERIAL);
        
        return new A9ID(wk, ts, sn);
    }
    
    // -------------------------------------------------------------
    private final int     worker;
    private final long    second;
    private final int     serial;
    private final long    number;
    private final boolean isBlack;
    
    /**
     * create an immutable A9ID instance.
     * 
     * @param worker range from 0 to 2^11 -1. 1 by default.
     * @param second range from 0 to 2^32 -1. 1 by default.
     * @param serial range from 0 to 2^20 -1. 1 by default.
     * @throws IllegalArgumentException if out of range
     */
    public A9ID(int worker, long second, int serial) {
        this(worker, second, serial, true);
    }
    
    private A9ID(int worker, long second, int serial, boolean isBlack) {
        
        // validate
        if (worker == 0) {
            if (!(RANGE_SECOND_STC.contains(second) || RANGE_SECOND_SYS.contains(second))) {
                ApiExceptions.checkRangeNotContains(RANGE_SECOND_STC, second);
                ApiExceptions.checkRangeNotContains(RANGE_SECOND_SYS, second);
            }
        }
        else {
            ApiExceptions.checkRangeNotContains(RANGE_WORKER, worker);
        }
        
        ApiExceptions.checkRangeNotContains(RANGE_SECOND, second);
        ApiExceptions.checkRangeNotContains(RANGE_SERIAL, serial);
        
        // set parts value
        this.worker = worker;
        this.second = second;
        this.serial = serial;
        this.isBlack = isBlack;
        
        // make number value
        long a9id = 0L;
        a9id |= ((long) worker) << (BIT_SECOND + BIT_SERIAL);
        a9id |= second << BIT_SERIAL;
        a9id |= serial;
        this.number = a9id;
    }
    
    /**
     * {@inheritDoc}
     */
    public int getWorker() {
        return worker;
    }
    
    /**
     * get the second
     * 
     * @return second
     */
    public long getSecond() {
        return second;
    }
    
    /**
     * get the serial
     * 
     * @return serial
     */
    public int getSerial() {
        return serial;
    }
    
    /**
     * black or red
     * 
     * @return true if black
     */
    public boolean isBlack() {
        return isBlack;
    }
    
    /**
     * {@inheritDoc}
     */
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(worker);
        sb.append(STRING_JOINER);
        sb.append(second);
        sb.append(STRING_JOINER);
        sb.append(serial);
        return sb.toString();
    }
    
    /**
     * {@inheritDoc}
     */
    public int compareTo(A9ID id) {
        if (id == null) {
            return -1;
        }
        if (this == id) {
            return 0;
        }
        return (int) (number - id.number);
    }
    
    /**
     * {@inheritDoc}
     */
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + worker;
        result = prime * result + (int) (second ^ (second >>> 32));
        result = prime * result + serial;
        return result;
    }
    
    /**
     * {@inheritDoc}
     */
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof A9ID)) {
            return false;
        }
        A9ID other = (A9ID) obj;
        if (worker != other.worker) {
            return false;
        }
        if (second != other.second) {
            return false;
        }
        if (serial != other.serial) {
            return false;
        }
        return true;
    }
    
    @Override
    public int intValue() {
        return (int) number;
    }
    
    @Override
    public long longValue() {
        return number;
    }
    
    @Override
    public float floatValue() {
        return (float) number;
    }
    
    @Override
    public double doubleValue() {
        return (double) number;
    }
}
